package com.wgo.bpot.server.service;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.apache.log4j.Logger;

import com.wgo.bpot.common.transport.exception.StaleDataException;
import com.wgo.bpot.common.transport.exception.db.DatabaseException;
import com.wgo.bpot.common.transport.servicefacade.DispatchEntry;
import com.wgo.bpot.common.transport.servicefacade.DispatchedModifyingMethodsTransport;
import com.wgo.bpot.common.transport.servicefacade.LightServices;
import com.wgo.bpot.common.transport.servicefacade.RetrieveResult;
import com.wgo.bpot.common.transport.servicefacade.SaveResult;
import com.wgo.bpot.common.transport.util.Assert;
import com.wgo.bpot.common.transport.util.ReflectionHelper;
import com.wgo.bpot.domain.common.CommandHistory;
import com.wgo.bpot.domain.common.CrudOperation;
import com.wgo.bpot.domain.common.UserSession;
import com.wgo.bpot.server.persist.PersistService;
import com.wgo.bpot.wiring.ServiceAccessor;
/**
 * The service implementation class.
 * 
 * @author petterei
 * @version $Id: LightServicesImpl.java,v 1.9 2006-02-06 09:22:24 petterei Exp $
 */
public class LightServicesImpl implements LightServices{
	
	private static final transient Logger log = Logger.getLogger(LightServicesImpl.class);
//	private TriggerRegistry triggerRegistry = new TriggerRegistryImpl(null) ; // FIXME: is this the best way to avoid exceptions, if not triggers is defined?
	private UserSession userSession;
    private ServiceAccessor serviceAccessor;
	private ModifyingOperationsState modifyingOperationsState = null ;
	
	
    @SuppressWarnings("unchecked")
    public LightServicesImpl(){
    }
    
    public void setServiceAccessor(ServiceAccessor serviceAccessor){
        this.serviceAccessor = serviceAccessor;
    }
    
	public ServiceAccessor getServiceAccessor() {
        return serviceAccessor;
    }

    public void setModifyingOperationsState(ModifyingOperationsState modifyingOperationsState) {
        this.modifyingOperationsState = modifyingOperationsState;
    }

    /**
	 * The user session is set by the RemoteServicesImpl
	 * @param userSession
	 */
	public void setUserSession(UserSession userSession) {
		this.userSession = userSession;
	}
	
	public UserSession getUserSession() {
		return userSession;
	}
	
	/**
	 * @see com.wgo.bpot.common.transport.servicefacade.LightServices#createDbSchema()
	 */
	public void createDbSchema(Class rootConceptClass) throws DatabaseException {
		Assert.assertNotNull(rootConceptClass, "The API-System-class cannot be null!");
		serviceAccessor.getInitializationService().initialize();
//		serviceAccessor.getPersistService().createSchema();
	}

	/**
	 * @see com.wgo.bpot.common.transport.servicefacade.LightServices#retrieveCompletePersistent(java.lang.Class, java.lang.Long, int)
	 */
	@SuppressWarnings("unchecked")
	public <T extends Object> T retrieveCompletePersistent(Class<T> facadeClass, Long identifier, int linkDepth) {
		T concept = (T) serviceAccessor.getPersistService().getInstance(facadeClass, identifier, linkDepth);
		if (null == concept) {
			log.warn("Could not load " + facadeClass + ", dbId: " + identifier + "from database.");
		} 
		RetrieveResult rr = new RetrieveResult();
		rr.setDomainConcept(concept);
		rr.setServerTime(System.currentTimeMillis());
		return (T) rr;
	}

	
	/**
	 * @see com.wgo.bpot.common.transport.servicefacade.LightServices#retrieveRequirementSystem(int)
	 */
	  @SuppressWarnings("unchecked")
	public <C> RetrieveResult<C> retrieveRootSystem(int linkDepth,Class<C> rootSystemApiClass){
		Assert.assertNotNull(rootSystemApiClass, "The API-System-class cannot be null!");
		List rootSystemList;
		try {
			rootSystemList = (List)serviceAccessor.getPersistService().find(rootSystemApiClass, linkDepth);
		} catch (Exception e) {
			throw new DatabaseException("Failed accessing or creating RootConcept!",e);
		}
		if ((null == rootSystemList) || (rootSystemList.size() < 1) ) {
			throw new DatabaseException ( "RootConcept not found, please initialize persistent layer." );
		}
		if (rootSystemList.size() > 1 ) {
			throw new DatabaseException ( "Inconsistent persistent layer: multiple RootConcepts not found." );
		}
		C concept = (C)rootSystemList.get(0);
		RetrieveResult<C> retrieveResult = new RetrieveResult<C>();
		retrieveResult.setDomainConcept(concept);
		retrieveResult.setServerTime(System.currentTimeMillis());
		log.info("retrieveRootSystem returns root concept of type: " + concept.getClass().getName());
		return retrieveResult;
	}

	/**
	 * @see com.wgo.bpot.common.transport.servicefacade.LightServices#save(DispatchedModifyingMethodsTransport[])
	 */
	@SuppressWarnings("unchecked")
	public SaveResult save(DispatchedModifyingMethodsTransport[] dirtyInfo) {
		boolean areChangesValid = (null == modifyingOperationsState) || modifyingOperationsState.validateChanges(dirtyInfo);
		if (!areChangesValid) {
			throw new StaleDataException();
		}
		SaveResult result = new SaveResult();
		result.setServerTime(System.currentTimeMillis());
		Set<Object> updatedConcepts = new HashSet<Object>();
		Set<Object> createdConcepts = new HashSet<Object>();
		int numConcepts = dirtyInfo.length;
		DatabaseLoadEntry[] dbEntries = new DatabaseLoadEntry[numConcepts];
		Map<Object, Object> implToEjb = new HashMap<Object, Object>();
		Map<Object, Set<DependentDispatch>> dirtyToDependentDispatches = new Hashtable<Object, Set<DependentDispatch>>();
		for (int i = 0; i < numConcepts ; i++) {
			Object dirtyConcept = dirtyInfo[i].getTargetConcept();
			Class conceptType = dirtyConcept.getClass().getInterfaces()[0] ;
			Object dbInstance = null;
			if (null == ReflectionHelper.invokeMethod(dirtyConcept, serviceAccessor.getDomainModelConvention().getIdentifierMethod(dirtyConcept))) { //TODO:reflection
				try {
					dbInstance = serviceAccessor.getPersistService().newInstance(conceptType);
                    serviceAccessor.getPersistService().save(dbInstance);
					createdConcepts.add(dbInstance);
				} catch (Throwable e) {
					throw new DatabaseException("Could not instantiate database instance from domain model " + dirtyConcept.getClass().getInterfaces()[0], e);
				}
			} else {
				if (null == dbInstance) {
					dbInstance = implToEjb.get(dirtyConcept);
				}				
				if (null == dbInstance) {
					dbInstance = load(conceptType, (Long)ReflectionHelper.invokeMethod(dirtyConcept, serviceAccessor.getPersistService().getDomainModelConvention().getIdentifierMethod(dirtyConcept)), serviceAccessor.getPersistService());
				}
				updatedConcepts.add(dbInstance);
			}
			implToEjb.put(dirtyConcept, dbInstance);
			dbEntries[i] =  new DatabaseLoadEntry(ReflectionHelper.<Serializable>invokeMethod(dbInstance, serviceAccessor.getPersistService().getDomainModelConvention().getIdentifierMethod(dbInstance)), conceptType);
			List<DispatchEntry> nonDependent = dirtyInfo[i].getNonDependentMethods();
			// invoke nondependent
			for (DispatchEntry entry : nonDependent) {
				ReflectionHelper.invokeMethod(dbInstance, entry.getMethod(), entry.getArgument());
			}
			// invoke outstanding dependencies
			if (dirtyToDependentDispatches.containsKey(dirtyConcept)) {
				Set<DependentDispatch> dependents = dirtyToDependentDispatches.remove(dirtyConcept);
				for (DependentDispatch dependent : dependents) {
					ReflectionHelper.invokeMethod(dependent.target, dependent.method, dbInstance);
				}
			}
			// invoke single dependent :
			List<DispatchEntry> singleDependent = dirtyInfo[i].getSingleDependentMethods();
			for (DispatchEntry entry : singleDependent) {
				if (null != entry.getArgument()) {
					processDependneMethodDispatching(entry.getArgument(), dbInstance, entry.getMethod(), implToEjb, dirtyToDependentDispatches, serviceAccessor.getPersistService());
				} else {
					ReflectionHelper.invokeMethod(dbInstance, entry.getMethod(), new Object[] {null});
				}
			}
			// invoke multiple dependent :
			List<DispatchEntry> multipleDependent = dirtyInfo[i].getMultipleDependentMethods();
			for (DispatchEntry entry : multipleDependent) {
				Method method = entry.getMethod();
				for (Object argument : (Collection<Object>)entry.getArgument()) {
					processDependneMethodDispatching(argument, dbInstance, method, implToEjb, dirtyToDependentDispatches, serviceAccessor.getPersistService());
				}
			}
		}
		// should never occure
		if (! dirtyToDependentDispatches.isEmpty()) { // should not occure, if using TestNG or other test-suites, watch out for class variables, they are modified by the suites..
			log.fatal("Unresolved dependencies. Size: " + dirtyToDependentDispatches.size());
			for (Entry<Object, Set<DependentDispatch>> entry: dirtyToDependentDispatches.entrySet()){
				String dependentDescription = "dependent: " + entry.getKey()+ ", dbId: " + ReflectionHelper.invokeMethod(entry.getKey(), serviceAccessor.getPersistService().getDomainModelConvention().getIdentifierMethod(entry.getKey()));
				log.fatal(dependentDescription);
				for (DependentDispatch dependent : entry.getValue()) {
					log.fatal(dependent.method.getName()+" should have been dispatched on " + dependent.target + "[dbId:"+ ReflectionHelper.invokeMethod(dependent.target, serviceAccessor.getPersistService().getDomainModelConvention().getIdentifierMethod(null))/*dependent.target.getDbId()*/+"], with "+ dependentDescription +" as argument" );
				}
			}		
			throw new DatabaseException("Unresolved dependencies. Size: " + dirtyToDependentDispatches.size());
		}
		for (Object ejbInstance : createdConcepts) {
//            assertValidOperation(ejbInstance, CrudOperation.CREATE);
			serviceAccessor.getTriggerRegistry().invokeBefore(CrudOperation.CREATE,ejbInstance);
		}
		for (Object ejbInstance : updatedConcepts) {
//            assertValidOperation(ejbInstance, CrudOperation.UPDATE);
			serviceAccessor.getTriggerRegistry().invokeBefore(CrudOperation.UPDATE,ejbInstance);
		}
		for (Object ejbInstance : createdConcepts) {
			serviceAccessor.getTriggerRegistry().invokeAfter(CrudOperation.CREATE,ejbInstance);
		}
		for (Object ejbInstance : updatedConcepts) {
			serviceAccessor.getTriggerRegistry().invokeAfter(CrudOperation.UPDATE,ejbInstance);
		}
		
		// load from database to ensure "WYSIWYG" - could be skipped to increase performance
		Object[] savedConceptsArray = new Object[numConcepts];
		for (int i = 0; i < numConcepts; i++) {
			savedConceptsArray[i] = serviceAccessor.getPersistService().getInstance( dbEntries[i].apiClazz, dbEntries[i].dbId , 1);
			Assert.assertNotNull(savedConceptsArray[i], "Retrieved saved instance from database server can not be 'null'!");
		}
		result.setSavedConcepts(savedConceptsArray);
		Set<CommandHistory> commandHistorySet = createCommandHistorySet(dirtyInfo);
		for (CommandHistory commandHistory : commandHistorySet) {
            serviceAccessor.getPersistService().save(commandHistory);
		}
		return result;
	}

//    @SuppressWarnings("unchecked")
//    private void assertValidOperation(Object ejbInstance, CrudOperation operation) {
//        Class conceptType = ejbInstance.getClass().getInterfaces()[0] ;
//        boolean isAllowed = userSession.getUser().getUserRole().isAllowedIfApplicable(service, operation, conceptType, ejbInstance);
//        if (!isAllowed) {
//            throw new InvalidServicePrivilegeException("User role " + userSession.getUser().getUserRole() + " is not allowed to perform " + operation + " on service " + service + " for type " + conceptType.getName());
//        }
//    }
	
	private Set<CommandHistory> createCommandHistorySet(DispatchedModifyingMethodsTransport[] dirtyInfo) {
		Set<CommandHistory> result = new HashSet<CommandHistory>();
		for (DispatchedModifyingMethodsTransport transport : dirtyInfo) {
			for (DispatchEntry entry : transport.getMultipleDependentMethods()) {
				result.add(createCommandHistory(entry,transport.getTargetConcept()));
			}
			for (DispatchEntry entry : transport.getNonDependentMethods()) {
				result.add(createCommandHistory(entry,transport.getTargetConcept()));
			}
			for (DispatchEntry entry : transport.getSingleDependentMethods()) {
				result.add(createCommandHistory(entry,transport.getTargetConcept()));
			}
		}
		Date time = new Date();
		int order = 0;
		for (CommandHistory history : result) {
			history.setDate(time);
			history.setUserName(userSession.getUser().getUserName());
			history.setOrder(order++);
		}
		return result;
	}

	private CommandHistory createCommandHistory(DispatchEntry entry, Object concept) {
		CommandHistory ch = serviceAccessor.getPersistService().createCommandHistoryEntry();
		Class conceptApiClass = serviceAccessor.getPersistService().getDomainModelConvention().implToApi(concept.getClass());
		ch.setConceptClass(conceptApiClass.getName());
		ch.setConceptDbId((Long)ReflectionHelper.invokeMethod(concept, serviceAccessor.getPersistService().getDomainModelConvention().getIdentifierMethod(concept)));
		ch.setConceptName(concept.toString());
		ch.setMethod(entry.getMethod().getName());
		if (null != entry.getArgument()) {
			Class argumentApiClass = null;
			try {
				argumentApiClass = serviceAccessor.getPersistService().getDomainModelConvention().implToApi(entry.getArgument().getClass());
			} catch (RuntimeException ignore) {
			}
			if (null != argumentApiClass) {
				ch.setArgumentClass(argumentApiClass.getName());
			} else {
				ch.setArgumentClass(entry.getArgument().getClass().getName());
			}
			String argStr = entry.getArgument().toString();
			if (100 < argStr.length()) {
				argStr = argStr.substring(0,100);
			}
			ch.setArgument(argStr);
			if (serviceAccessor.getPersistService().getDomainModelConvention().getDomainModelInterface().isInstance(entry.getArgument())) {
				Object p = entry.getArgument();
				ch.setArgumentDbId((Long)ReflectionHelper.invokeMethod(p, serviceAccessor.getPersistService().getDomainModelConvention().getIdentifierMethod(p)));
			}
		}
		return ch;
	}

	private void processDependneMethodDispatching(Object argument, Object targetDbInstance, Method method, Map<Object, Object> domainToEjb, Map<Object, Set<DependentDispatch>> dirtyToDependentDispatches, PersistService persistService ) {
		Object ejbArgument = domainToEjb.get(argument);
		if (null == ejbArgument) {
			if (null == ReflectionHelper.invokeMethod(argument, persistService.getDomainModelConvention().getIdentifierMethod(argument))) {
				Set<DependentDispatch> dependentDispatches = dirtyToDependentDispatches.get(argument); 
				if (null == dependentDispatches) {
					dependentDispatches = new HashSet<DependentDispatch>();
					dirtyToDependentDispatches.put(argument, dependentDispatches);
				}
				dependentDispatches.add(new DependentDispatch(method, targetDbInstance));
				return;
			}
			ejbArgument = load(argument.getClass().getInterfaces()[0], (Long)ReflectionHelper.invokeMethod(argument, persistService.getDomainModelConvention().getIdentifierMethod(argument)), persistService);
		}
		ReflectionHelper.invokeMethod(targetDbInstance, method, ejbArgument);
	}

//	@SuppressWarnings("unchecked")
//	private Persistent load (Persistent domain, Long dbId, PersistService persistService) {
//		Class<Persistent> ejbImplClass = ServerModelConverter.loadServerClass(domain.getClass().getInterfaces()[0]);
//		return load(ejbImplClass, dbId, persistService);
//	}
	private Object load (Class apiType, Long dbId, PersistService persistService) {
		Object dbInstance = persistService.getDbInstance(apiType, dbId);
		if (null == dbInstance) {
			throw new DatabaseException("Could not load dbInstance from database: " + apiType);
		}
		return dbInstance;
	}
	
	public PersistService getPersistService(){
		return serviceAccessor.getPersistService();
	}
	
	private class DatabaseLoadEntry {
		private Serializable dbId = null;
		private Class apiClazz = null;
		private DatabaseLoadEntry(Serializable dbId, Class apiClazz) {
			this.dbId = dbId;
			this.apiClazz = apiClazz;
		}
	}
	
	private class DependentDispatch {
		Method method = null;
		Object target = null;
		private DependentDispatch(Method method, Object target) {
			this.method = method;
			this.target = target;
		}
	}
	
	public Long getServerTime() {
		return System.currentTimeMillis();
	}

	public List<CommandHistory> retrieveCommandHistory(Object persistent) {
		if (null != persistent) {
			Class apiClass = serviceAccessor.getPersistService().getDomainModelConvention().implToApi(persistent.getClass());
			List<CommandHistory> cmdHistList = serviceAccessor.getPersistService().retrieveCommandHistory(apiClass,(Long)ReflectionHelper.invokeMethod(persistent, serviceAccessor.getPersistService().getDomainModelConvention().getIdentifierMethod(persistent)));
			return cmdHistList;
		}
		return serviceAccessor.getPersistService().retrieveCommandHistory(null,null);
	}

}